package sqlc

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/jmoiron/sqlx"
	"github.com/redis/go-redis/v9"
)

const (
	placeholder        = "#placeholder"
	defaultExpire      = 30 * time.Second
	defaultEmptyExpire = 10 * time.Second
)

var (
	placeholderBytes  = []byte(placeholder)
	ErrRecordNotFound = errors.New("record not found")
)

type (
	ExecCtxFn func(ctx context.Context, conn *sqlx.DB) error
)

type SqlCache struct {
	db          *sqlx.DB
	redis       redis.UniversalClient
	expire      time.Duration
	emptyExpire time.Duration
}

func NewSqlCache(db *sqlx.DB, redis redis.UniversalClient) *SqlCache {
	return &SqlCache{
		db:          db,
		redis:       redis,
		expire:      defaultExpire,
		emptyExpire: defaultEmptyExpire,
	}
}

func (g *SqlCache) Create(ctx context.Context, exec ExecCtxFn, key ...string) error {
	err := exec(ctx, g.db)
	if err != nil {
		return err
	}
	err = g.DeleteCache(ctx, key...)
	if err != nil {
		return err
	}
	return nil
}

func (g *SqlCache) Update(ctx context.Context, exec ExecCtxFn, key ...string) error {
	err := exec(ctx, g.db)
	if err != nil {
		return err
	}
	err = g.DeleteCache(ctx, key...)
	if err != nil {
		return err
	}
	return nil
}

func (g *SqlCache) Delete(ctx context.Context, exec ExecCtxFn, key ...string) error {
	err := exec(ctx, g.db)
	if err != nil {
		return err
	}
	err = g.DeleteCache(ctx, key...)
	if err != nil {
		return err
	}
	return nil
}

func (g *SqlCache) Find(ctx context.Context, data interface{}, key string,
	exec ExecCtxFn,
	opts ...FindOptions,
) error {
	o := &findOption{
		expire:      g.expire,
		emptyExpire: g.emptyExpire,
		useCache:    true,
	}
	for _, opt := range opts {
		opt(o)
	}
	// 不使用redis
	if !o.useCache {
		err := exec(ctx, g.db)
		if err != nil {
			return err
		}
		return nil
	}
	b, err := g.redis.Get(ctx, key).Bytes()
	if err != nil {
		if !errors.Is(err, redis.Nil) {
			return err
		}
		//redis未找到，从db中获取
		err := exec(ctx, g.db)
		if err != nil {
			if errors.Is(err, ErrRecordNotFound) {
				//db中未找到，放入默认值防止击穿
				_, err2 := g.redis.Set(ctx, key, placeholder, o.emptyExpire).Result()
				if err2 != nil {
					return fmt.Errorf("set placeholder error: %w", err2)
				}
			}
			return err
		}
		//db中找到，写入redis
		err = g.SaveCache(ctx, key, data, o.expire)
		if err != nil {
			return err
		}
		return nil
	}
	//redis找到默认值
	if bytes.Equal(b, placeholderBytes) {
		return ErrRecordNotFound
	}
	err = json.Unmarshal(b, data)
	if err != nil {
		return err
	}
	return nil
}

func (g *SqlCache) DeleteCache(ctx context.Context, key ...string) error {
	_, err := g.redis.Del(ctx, key...).Result()
	if err != nil {
		return err
	}
	return nil
}

func (g *SqlCache) SaveCache(ctx context.Context, key string, val interface{}, expire time.Duration) error {
	b, err := json.Marshal(val)
	if err != nil {
		return err
	}
	_, err = g.redis.Set(ctx, key, string(b), expire).Result()
	if err != nil {
		return err
	}
	return nil
}
